package com.automatastudios.notifications {
	import flash.errors.IllegalOperationError;
	import flash.utils.Dictionary;
	
	/**
	 * The NotificationCenter provides a central way broadcast information in an application. It takes Notification objects and sends them to any 
	 * listener that has registered to receive the notification.  Not to be mistaken as an event dispatcher, the NotificationCenter provides a more 
	 * robust way to pass messages in an application while creating a layer of abstraction between application parts.  Observers can register to 
	 * receive notifications in one of four ways:
	 * <ul>
	 * <li>register to receive notifications of a specific type broadcast by a specific sender</li>
	 * <li>register to receive notifications of a specific type broadcast by any sender</li>
	 * <li>register to receive notifications of any type broadcast by a specific sender<li>
	 * <li>register to receive all notifications</li>
	 * </ul>
	 * 
	 * <p>When an sender wishes to notify observers, it sends a Notification object to the NotificationCenter, which then sends the 
	 * notification object to all observers that registered to be notified.  The sender can choose to attach data to the notification object, via the 
	 * senderData property, that observers may need.  Also, when an observer registers to receive a notification, it can 
	 * optionally attach data to the registration.  Any time the observer receives a notification that matches this registration, the attached 
	 * observerData is also returned.
	 * </p>
	 * 
	 * <p>The notification center notifies observers synchronously, which is to say that the notify() method does not return until all 
	 * observers have received the notification.
	 * 
	 * @see Notification
	 */
	public class NotificationCenter {
		private static var _instance:NotificationCenter;
		private static var singleton:Boolean = false;
		
		private var _observerNameKeyTable:Object;
		private var _observerObjectKeyTable:Dictionary;
		private var _observerFullKeyTable:Object;
		private var _observerNoKeyTable:Array;
		
		/**
		 * The NotificationCenter constructor is actually a private constructor since NotificationCenter is a singleton.  If you try to call this 
		 * constructor, you will receive a compile-time error.  Instead, us the getInstance method.
		*/
		public function NotificationCenter() {
			if (!singleton) {
				throw new IllegalOperationError("Instance of this class can only be obtained by calling the getInstance() method.");
			} else {
				_observerNameKeyTable = new Object();
				_observerObjectKeyTable = new Dictionary();
				_observerFullKeyTable = new Object();
				_observerNoKeyTable = new Array();
			}
		}
		
		/**
		 * The getInstance() method returns the instance of the NotificationCenter.  NotificationCenter is a singleton, so only one instance may exist 
		 * at any time.
		*/
		public static function getInstance():NotificationCenter {
			singleton = true;
			if (!_instance) {
				_instance = new NotificationCenter();
				singleton = false;
			}
			
			return _instance;
		}
		
		/**
		 * The addObserver() method adds an entry to the NotificationCenter's dispatch table with an observer, optional notification name, optional 
		 * notification sender, and optional observer data. This method is used to register to receive notifications from senders in one of four ways:
		 * 
		 * <ul>
		 * <li>register to receive notifications of a specific type broadcast by a specific sender</li>
		 * <li>register to receive notifications of a specific type broadcast by any sender</li>
		 * <li>register to receive notifications of any type broadcast by a specific sender<li>
		 * <li>register to receive all notifications</li>
		 * </ul>
		 * 
		 * <p>Note: A single observer can be added multiple times as long as each time it is added, is registers using a different criteria.</p>
		 * 
		 * @param observer function to call when a notification is received that matches the registering criteria
		 * @param notificationName optional name of the notification for which the observer wishes to receive
		 * @param notificationSender optional object from which the observer wishes to receive notifications
		 * @param observerData optional data the observer wishes to receive any time a notification is received that matches the registering criteria
		 * 
		 * @see #removeObserver()
		*/
		public function addObserver(observer:Function, notificationName:String = null, notificationSender:Object = null, observerData:* = null):void {
			if (notificationName && notificationSender) {
				addObserverByFullKey(observer, notificationName, notificationSender, observerData);
			}
			else if (notificationName) {
				addObserverByNameKey(observer, notificationName, observerData);
			}
			else if (notificationSender) {
				addObserverByObjectKey(observer, notificationSender, observerData);
			}
			else {
				addObserverByNoKey(observer, observerData);
			}
		}
		
		/**
		 * The removeObserver() method removes all references to an observer from the NotificationCenter. The removed observer will not receive any more 
		 * notifications.
		 * 
		 * @ param observer the observer to be removed
		 * 
		 * @see #addObserver()
		*/
		public function removeObserver(observer:Function):void {
			var name:String;
			var sender:Object;
			var i:uint;
			var max:uint;

			// remove observer from full key table
			for (name in _observerFullKeyTable) {
				for (sender in _observerFullKeyTable[name]) {
					max = _observerFullKeyTable[name][sender].length;
					for (i = 0; i < max; ++i) {
						if (_observerFullKeyTable[name][sender][i].observer == observer) {
							_observerFullKeyTable[name][sender].splice(i, 1);
						}
					}
				}
			}
			
			// remove observer from name key table
			for (name in _observerNameKeyTable) {
				max = _observerNameKeyTable[name].length;
				for (i = 0; i < max; ++i) {
					if (_observerNameKeyTable[name][i].observer == observer) {
						_observerNameKeyTable[name].splice(i, 1);
					}
				}
			}
			
			// remove observer from object key table
			for (sender in _observerObjectKeyTable) {
				max = _observerObjectKeyTable[sender].length;
				for (i = 0; i < max; ++i) {
					if (_observerObjectKeyTable[sender][i].observer == observer) {
						_observerObjectKeyTable[sender].splice(i, 1);
					}
				}
			}
			
			// rmeove observer from no key table
			max = _observerNoKeyTable.length;
			for (i = 0; i < max; ++i) {
				if (_observerNoKeyTable[i].observer == observer) {
					_observerNoKeyTable.splice(i, 1);
				}
			}
		}
		
		/**
		 * The notify() method is used by a sender to send a notification.  The sender creates a Notification object, and calls notify() to 
		 * broadcast that notification. The Notification object contains the name of the notification, a reference to the sender of the notification, 
		 * and any data the sender wishes to send with the notification. The NotificationCenter sends that notification to all qualified observers 
		 * (adding the observerData if necessary).
		 * 
		 * @param notification the notification to broadcast
		*/
		public function notify(notification:Notification):void {
			var i:uint;
			var max:uint = 0;
			
			// notify full key observers
			if (_observerFullKeyTable[notification.name]) {
				if (_observerFullKeyTable[notification.name][notification.sender]) {
					max = _observerFullKeyTable[notification.name][notification.sender].length;
					for (i = 0; i < max; ++i) {
						notification.observerData = _observerFullKeyTable[notification.name][notification.sender][i].observerData;
						_observerFullKeyTable[notification.name][notification.sender][i].observer(notification);
					}
				}
			}
			
			// notify name key observers
			max = 0;
			if (_observerNameKeyTable[notification.name]) {
				max = _observerNameKeyTable[notification.name].length;
				for (i = 0; i < max; ++i) {
					notification.observerData = _observerNameKeyTable[notification.name][i].observerData;
					_observerNameKeyTable[notification.name][i].observer(notification);
				}
			}
			
			// notify object key observers
			max = 0;
			if (_observerObjectKeyTable[notification.sender]) {
				max = _observerObjectKeyTable[notification.sender].length;
				for (i = 0; i < max; ++i) {
					notification.observerData = _observerObjectKeyTable[notification.sender][i].observerData;
					_observerObjectKeyTable[notification.sender][i].observer(notification);
				}
			}
			
			// notify no key observers
			max = _observerNoKeyTable.length;
			for (i = 0; i < max; ++i) {
				notification.observerData = _observerNoKeyTable[i].observerData;
				_observerNoKeyTable[i].observer(notification);
			}
		}
		
		private function addObserverByFullKey(observer:Function, notificationName:String, notificationSender:Object, observerData:* = null):void {
			if (!_observerFullKeyTable[notificationName]) {
				_observerFullKeyTable[notificationName] = new Dictionary();
			}
			
			if (!_observerFullKeyTable[notificationName][notificationSender]) {
				_observerFullKeyTable[notificationName][notificationSender] = new Array();
			}

			_observerFullKeyTable[notificationName][notificationSender].push({observer:observer, observerData:observerData});
		}
		
		private function addObserverByNameKey(observer:Function, notificationName:String, observerData:* = null):void {
			if (!_observerNameKeyTable[notificationName]) {
				_observerNameKeyTable[notificationName] = new Array();
			}
			
			_observerNameKeyTable[notificationName].push({observer:observer, observerData:observerData});
		}
		
		private function addObserverByObjectKey(observer:Function, notificationSender:Object, observerData:* = null):void {
			if (!_observerObjectKeyTable[notificationSender]) {
				_observerObjectKeyTable[notificationSender] = new Array();
			}
			
			_observerObjectKeyTable[notificationSender].push({observer:observer, observerData:observerData});
		}
		
		private function addObserverByNoKey(observer:Function, observerData:* = null):void {
			_observerNoKeyTable.push({observer:observer, observerData:observerData});
		}
	}
}